Java设计模式
Java 设计模式
一、基础
1.1 - 设计模式的重要性
- 软件工程中,设计模式是对软件设计中普遍存在的各种问题所提出的解决方案。
- 设计模式用在:
- 面向对象 (
oop
) => 功能模块 [ 设计模式 + 算法 ( 数据结构 ) ] => 框架 [ 使用到多种设计模式 ] => 架构 [ 服务器集群 ]- 是程序员在编程时应该遵守的原则,也是各种设计模式的基础
1.2 - 设计模式的目的
编写软件过程中,程序员面临着来自耦合性、内聚性以及可维护性,可扩展性,重用性,灵活性等多方面挑战,设计模式是为了让程序具有更好的性能
- 代码重用性 – 功能相同的代码,不用多次编写
- 可读性 – 编程规范性,便于其他人阅读和理解
- 可扩展性 – 当需要增加新的功能时,非常的方便,成为可维护
- 可靠性 – 当我们增加新的功能后,对原来的功能没有影响
- 是程序呈现高内聚,低耦合的特性
1.3 - 设计模式的七大原则
设计原则的核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力
1.3.1 单一职责原则
- 降低类的复杂度,一个类只负责一项职责
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在方法级违反单一职责原则
Demo1
1 | public class SRP_Demo01 { |
Demo2
1 | public class SRP_Demo02 { |
Demo3
1 | public class SRP_Demo03 { |
1.3.2 接口隔离原则
- 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
Demo1
1 | public class ISP_Demo01 { |
Demo2
1 | public class ISP_Demo02 { |
1.3.3 依赖倒转原则
- 高层模块不应该依赖底层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应依赖抽象
- 依赖倒转的中心思想是面向接口编程
- 相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定得多,在
Java
中,抽象指的是接口或抽象类,细节就是指具体的实现类- 使用接口或者抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给实现类
依赖关系传递的三种方式和应用案例
- 接口传递
- 构造方法传递
setter
方式传递
注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性良好
- 变量的声明类型尽量使抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
Demo1
1 | public class DIP_Demo01 { |
Demo2
1 | public class DIP_Demo02 { |
1.3.4 里氏替换原则
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏
- 继承在程序设计带来便利的同时也带来了弊端,比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
- 如果对每个类型为
T1
的对象O1
,都有类型为T2
的对象O2
,使得以T1
定义的所有程序P
在所有的对象O1
都代换成O2
时,程序P
的行为没有发生变化,那么类型T2
是类型T1
的子类型。换句话来说,所有引用基类的地方必须能透明的使用其子类的对象- 在使用继承时,在子类中尽量不要重写父类的方法
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
Demo1
1 | public class LSP_Demo01 { |
Demo2
1 | public class LSP_Demo02 { |
1.3.5 开闭原则
- 一个软件实体,如类、模块、函数,应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
- 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则
Demo1
1 | public class OCP_Demo01 { |
Demo2
1 | public class OCP_Demo02 { |
1.3.6 迪米特原则
- 一个对象应该是对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特原则又交最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类,不管多么复杂,都尽量将逻辑封装在类的内部,对外除了提供
public
方法,不对外泄露任何信息- 只与直接的朋友通信
- 直接的朋友 – 每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就称这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
注意事项及细节
- 迪米特原则的核心是降低类之间的耦合
- 由于每个类都减少了不必要的依赖,因此迪米特原则只是要求降低类间的耦合关系,并不是完全没有依赖
Demo1
1 | public class DP_Demo01 { |
Demo2
1 | public class DP_Demo02 { |
1.3.7 合成复用原则
- 原则是尽量使用合成/聚合的方式,而不是使用继承
- 如果只是想让 B 类去使用 A 类的方法,使用继承就会让 B 和 A 的耦合性增强
- 依赖 – B 类中有方法的参数是 A 类型
- 聚合 – B 类中有 A 类的属性,通过 setter 设置 A 类的值
- 组合 – B 类中有 A 类的属性,在创建 B 的时候直接实例化 A 的值
1.4 - UML 类图
基本介绍
UML (Unified modeling language)
– 统一建模语言,是一种用于软件系统分析和设计的语言工具,用于帮助软件开发人员进行思考和记录思路的结果。- 本身是一套符号的规定,就像数学符号和化学符号一样,这些符号用于描述软件模型中的各个元素和他们之间关系,比如类、接口、实现、泛化、依赖、组合、聚合等
- 使用
UML
来建模,常用的工具有Rational Rose
,也可以用一些插件来建模
Dependency
– 依赖Association
– 关联Generalization
– 泛化/继承Realization
– 实现Aggregation
– 聚合Composite
– 组合
二、单例模式
采取一定的方法,保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
2.1 - 源码追溯
1 | public class Runtime { |
2.2 - 注意事项和细节说明
- 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
- 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用
new
- 单例模式使用的场景:
- 需要频繁的进行创建和销毁的对象
- 创建对象时耗时过多或耗费资源过多,但又经常要用到的对象
- 工具类对象
- 频繁访问数据库或文件的对象
2.3 - 八种方式
2.3.1 饿汉式 - 静态常量
- 构造器私有化(防止 new )
- 类的内部创建对象
- 向外暴露一个静态方法
- 代码实现
优点
这种写法比较简单,就是在类加载的时候就完成实例化。避免了线程同步的问题
缺点
在类装载的时候就完成实例化,没有达到
Lazy Loading
的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
总结
这种单例模式可用,可能造成内存浪费
Demo
1 | public class STtest { |
2.3.2 饿汉式 - 静态代码块
优缺点
- 这种方式和静态常量方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的
- 结论 – 这种单例模式可用,可能会造成内存浪费
Demo
1 | public class STtest { |
2.3.3 懒汉式 – 线程不安全
优缺点
- 起到了
Lazy Loading
的效果,但只能在单线程下使用- 如果在多线程下,一个线程进入了判空,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式
- 结论 – 在实际开发中,不要使用这种方式
Demo
1 | public class STtest { |
2.3.4 懒汉式 – 线程安全,同步方法
优缺点
- 解决了线程不安全问题
- 效率太低了,每个线程在想获得类的实例的时候,执行获取方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面想获得该类实例直接
return
就好,方法进行同步效率太低- 结论 – 在实际开发中,不推荐使用这种方式
Demo
1 | public class STtest { |
2.3.5 懒汉式 – 线程安全,同步代码块
优缺点
- 本意是想对第四种方式的改进,因为前面同步方法效率太低,改进为同步产生实例化的代码块
- 但这种方法并不能起到线程同步的作用。原因和第三种方式是一样的
- 结论 – 在实际开发中,不能使用这种方式
Demo
1 | public class STtest { |
2.3.6 双重检查
- 是在多线程开发中常用到的
- 避免反复进行同步方法
- 线程安全,延迟加载,效率较高
- 结论 – 在实际开发中,推荐使用这种单例设计模式
Demo
1 | public class STtest { |
2.3.7 静态内部类
静态内部类的特点
- 当外层的类装载的时候,内部的类不会被装载
- 调用静态内部类的方法时,静态内部类会被装载,并且只装载一次,线程是安全的
优缺点
- 采用了类装载机制来保证初始化实例时只有一个线程
- 静态内部类方式在外部类被装载时并不会立即实例化,而是在需要实例化时,调用方法,才会装载静态内部类,从而完成外部类的实例化
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,
JVM
帮我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的- 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论 – 推荐使用
Demo
1 | public class STtest { |
2.3.8 枚举
优缺点
- 借助枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
- 结论 – 推荐使用
Demo
1 | public class STtest { |
三、工厂模式
引入一个具体的需求
- 披萨的种类很多,比如
GreekPizz
、CheesePizz
- 披萨的制作有
prepare
、bake
、cut
、box
- 完成披萨店的订购功能
3.1 - 源码追溯
1 | Calendar calendar = Calendar.getInstance(); |
1 | public static Calendar getInstance() { |
1 | private static Calendar createCalendar(TimeZone zone, Locale aLocale) { |
3.2 - 三种模式
3.2.1 简单工厂模式
- 简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式中最简单实用的模式
- 定义了一个创建对象的类,由这个类来封装实例化对象的行为
- 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会用到工厂模式
增加新的需求
客户在点披萨时,可以点不同口味的披萨,比如北京的奶酪
pizza
,北京的胡椒pizza
或者伦敦的奶酪pizza
,伦敦的胡椒pizza
3.2.2 工厂方法模式
3.2.3 抽象工厂模式
- 定义了一个
interface
用于创建相关或有依赖关系的对象簇,而无需指明具体的类- 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合
- 从设计层面看,抽象工厂模式就是对简单工厂模式的改进
- 将工厂抽象成两层,抽象工厂和具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类,这样将简单工厂类变成了工厂簇,更利于代码的维护和扩展
3.3 - 工厂模式小结
- 工厂模式的意义 – 将实例化对象的代码提取出来,放到一个类中统一管理和为何,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性
- 三种工厂模式
- 设计模式的依赖抽象原则
- 创建对象实例时,不要直接 new 类,而是把这个 new 类的动作放在一个工厂的方法中,并返回。
- 不要让类继承具体类,而是继承抽象类或者是实现
interface
接口- 不要覆盖基类中已经实现的方法
四、原型模式
引入一个具体需求
现有一只羊
tom
,年龄为1
,颜色为white
请编写程序创建和tom
属性完全相同的10
只羊
4.1 - 源码追溯
4.2 - 基本介绍
- 用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
- 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 工作原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即
对象.clone()
4.3 - 深入讨论
4.3.1 浅拷贝
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象
- 对于数据类型是引用数据类型的成员变量,比如是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会印象另一个对象的该成员变量值
4.3.2 深拷贝
- 复制对象的所有基本数据类型的成员变量值
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,知道该对象可达的所有对象。也就是说对象进行深拷贝要对整个对象进行拷贝
- 实现方法
- 重写 clone() 方法
- 通过对象序列化
4.4 - 注意事项和细节
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能提高效率
- 不用重写初始化对象,而是动态的获得对象运行时的状态
- 如果原始对象发生变化(增加/减少属性),其他克隆对象也会发生相应的变化,无需修改代码
- 在实现深克隆的时候可能需要比较复杂的代码
- 缺点 – 需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其原代码,违背了
OCP
原则
五、建造者模式
引入一个具体需求
盖房子需求
- 需要建房子:这一过程为打桩、砌墙、封顶
- 房子有各种各样的,比如普通房,高楼,别墅,各种房子的过程虽然一样,但是要求不要相同
5.1 - 基本介绍
- 又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来。是这个抽象过程的不同实现方法可以构造出不同表现的对象
- 是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节
5.2 - 建造者模式的四个角色
Product
– 产品角色,一个具体的产品对象Builder
– 抽象建造者,创建一个Product
对象的各个部件指定的接口ConcreteBuilder
– 具体建造者,实现接口,构建和装配各个部件Director
– 构建一个使用Builder
接口的对象。它主要是用于创建一个复杂的对象。主要有两个作用
- 隔离了客户于对象的生产过程
- 负责控制产品对象的生产过程
5.3 - 注意事项和细节
- 客户端(使用程序)不必要知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便的替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象
- 可以增加精细地控制产品地创建过程。将复杂产品地创建步骤分解在不同地方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
- 增加新的具体建造者无需修改原有类库地代码,指挥者类针对抽象建造者类变成,系统扩展方便,符合
OCP
- 建造者模式所创建的产品一般具有较多的相同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围收到一定的限制
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式
- 抽象工厂模式 vs 建造者模式
- 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心产品由什么工厂完成即可
- 建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零件而产生一个新产品
六、适配器模式
6.1 - 基本介绍
- 适配器模式将某个类地接口装换成客户端期待地另一个接口表示,主要目的是兼容性,让原本因接口不匹配不能一起工作地两个类可以协同工作,其别名为包装器
- 适配器属于结构型模式
6.2 - 工作原理
- 将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容
- 从用户的角度看不见被适配者,是解耦的
- 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法
- 用户接收到返回结果,感觉只是和目标接口交互
6.3 - 三种方式
6.3.1 类适配器模式
模式介绍
Adapter
类,通过继承src
类,实现dst
类接口,完成src
->dst
的适配
应用实例
- 以生活中充电器的例子来讲解适配器,充电器本身相当于
Adapter
,220v
交流电相当于src
,我们的dst
是5v
的直流电
注意事项和细节
Java
是单继承机制,所以类适配器需要继承src
类这一点算是一个缺点,因为这要求dst
必须是接口,有一定局限性src
类的方法在Adapter
中会暴露出来,也增加了使用的成本- 由于其继承了
src
类,所以它可以根据需求重写src
类的方法,使得Adapter
的灵活性增加
6.3.2 对象适配器模式
基本介绍
- 基本思路和类的适配器模式相同,,只是将
Adapter
类作修改,不是继承src
类,而是持有src
类的实例,以解决兼容性的问题。即:持有src
类,实现dst
类接口,完成src
->dst
的适配- 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系
注意事项和细节
- 根据合成复用原则,使用组合代替继承,所以它解决了类适配器必须继承
src
的局限性问题,也并不再要求dst
必须是接口- 使用成本更低,更灵活
6.3.3 接口适配器模式
基本介绍
- 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中的每一个方法提供一个默认的实现方法(空方法),那么该抽象类的子类可有选择的覆盖父类的某些来实现需求
- 适用于一个接口不想使用其所有方法的情况
6.4 - 注意事项和细节
- 类适配器 – 以类给到,在
Adapter
里,就是将src
当作类继承- 对象适配器 – 以对象给到,在
Adapter
里,将src
作为一个对象持有- 接口适配器 – 以接口给到,在
Adapter
里,将src
作为一个接口实现Adapter
模式最大的作用还是将原本不兼容的接口融合在一起工作- 实际开发中,实现起来不拘泥于我们讲解的三种经典模式
七、桥接模式
7.1 - 基本介绍
- 将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变
- 是一种结构型设计模式
- 基于类的最小设计模式,通过封装、聚合、继承等行为让不同的类承担不同的职责,主要特点使把抽象与行为实现分离开来,从而可以保持各部分的独立性以及应对它们的功能扩展
7.2 - 注意事项及细节
- 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和是心啊部分独立开来,者有助于系统进行分层设计,从而产生更好的结构化系统
- 对于系统的高层部分,只要知道抽象部分和实现部分的接口就可以了,其他部分由具体业务来完成
- 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本
- 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用具体范围有一定的局限性,即需要有这样的应用场景
八、装饰者模式
8.1 - 基本介绍
- 动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则
OCP
九、组合模式
9.1 - 基本介绍
- 又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示整体-部分的层次关系
- 组合模式依据树形结构来组合对象,用来表示部分以及整体层次
- 这种类型的设计模式属于结构型模式
- 组合模式使得用户对单个对象和组合对象的访问具有一致性,即组合能让客户以一致的方式处理个别对象以及组合对象
9.2 - 注意事项及细节
- 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题
- 具有较强的扩展性。当我们要更改组合对象时,我们只需要调正内部的层次关系,客户端不需要做出任何改动
- 方便创建出复杂的层次结构。客户端不用理会组合里的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
- 需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式
- 要求较高的抽象性,如果节点和叶子节点有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
十、外观模式
10.1 - 基本介绍
- 也叫过程模式,外观模式为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
- 外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端口只需跟这个接口发生调用,而无需关心这个子系统的内部细节
10.2 - 注意事项和细节
- 对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
- 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更容易维护和扩展
- 通过合理的使用外观模式,可以帮我们更好的划分访问层次
- 当系统需要进行分层设计师,可以考虑使用外观模式
- 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和发展,此时可以考虑为新的系统开发一个外观模式类,来提供遗留系统的比较清晰简单的接口,让新的系统与外观模式类交互,提高复用性
- 不能过多的或者不合理的使用外观模式,使用外观模式号,还是直接调用模块号。要以让系统有层次,利于维护为目的
十一、享元模式
11.1 - 基本介绍
- 又叫蝇量模式,运用共享技术有效的支持大量细粒度的对象
- 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,这这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
- 享元模式能够解决重复对象的内存浪费问题,当系统中有大量相似对象,需要缓冲池时,不需要总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
11.2 - 内部状态和外部状态
- 享元模式提出了两个要求:细粒度和共享对象,这里就涉及到内部状态和外部状态了,即对象的信息分为两个部分
- 内部状态 – 即对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
- 外部状态 – 即对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
11.3 - 注意事项及细节
- 享 - “共享”,元 - “对象”
- 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
- 用唯一识别码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用
HashMap
/HashTable
存储- 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
- 享元模式提高了系统的复杂度。需要分理处内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方
- 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
- 享元模式经典的应用场景是需要缓冲池的场景
十二、代理模式
12.1 - 基本介绍
- 代理模式:作为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象事项的基础上,增强额外的功能操作,即扩展目标对象的功能
- 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
- 代理模式有不同的形式、主要有三种
- 静态代理 – 基于接口
- 动态代理 – 基于接口,也称
JDK代理
或接口代理
- Cglib代理 – 可以在内存动态创建对象,而不需要实现接口
12.2 - 几种模式
12.2.1 静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类
- 优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展
- 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理对象嘞
- 一旦接口增加方法,目标对象和代理对象都要维护
12.2.2 动态代理
- 代理对象不需要实现接口,代理目标对象要实现接口,否则不能动态代理
- 代理对象的生成时利用
JDK
的API
,动态的在内存中构建代理对象- 动态代理也叫做:
JDK代理
、接口代理
1 | static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) |
12.2.3 Cglib代理
- 静态代理和
JDK
代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理Cglib代理
也叫做子类代理,它是在内存中构建一个子类对象,从而实现对目标对象功能扩展,Cglib
是一个强大的高性能的代码生成包,它可以再运行期扩展Java
类与实现Java
接口,它广泛的被许多AOP
的框架使用,例如Spring AOP
,实现方法蓝杰- 在
AOP
编程中如何选择代理模式:
- 目标对象需要实现接口,用
JDK
代理- 目标对象不需要实现接口,用
Cglib
代理Cglib包
的底层是通过使用字节码处理框架ASM
来转换字节码并生成新的类
实现步骤
- 需要引入
Cglib
的jar
包- 在内存中动态构建子类,注意代理的类不能为
final
,否则报错java.lang.illegalArgumentException
- 目标对象的方法如果为
final/static
,那么就不会被拦截,即不会执行目标对象额外的业务方法
十三、模板方法模式
13.1 - 基本介绍
- 又交模板模式,在一个抽象类公开定义了执行它的方法的模板,它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
- 简单说,模板方法模式定义了一个操作中算法的股价,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
- 属于行为型模式
13.2 - 钩子方法
- 在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以事情框框要不要覆盖他,该方法成为“钩子”
十四、命令模式
14.1 - 基本介绍
- 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用我们的命令模式来进行设计
- 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦
- 在命令模式中,会将一个请求封装为一个对象,一边使用不同参数来标识不同的请求,同时命令模式也支持可撤销的操作
Invoker
是调用者Receiver
是被调用者MyCommand
是命令
14.2 - 注意事项及细节
- 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的
execute()
方法就可以让接收者工作,而不必知道具体的接收者对象是谁、如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:“请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用- 容易设计一个命令队列,只要把命令对象放到队列,就可以多线程的执行命令
- 容易实现对请求的撤销和重做
- 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在使用的时候要注意
- 空命令也是一种设计模式,它为我们省去了判空的操作
- 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟
CMD
订单的撤销/恢复、触发-反馈机制
十五、访问者模式
15.1 - 基本介绍
- 访问者模式,封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作
- 主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题
- 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
- 访问者模式的主要应用场景: 需要对一个对象结构中的对象进行很多不同操作,这些操作彼此没有关联,同时需要避免让这些操作“污染”这些对象的类,可以选用访问者模式解决
15.2 - 注意事项及细节
优点
- 访问者模式符合单一职责原则,让程序具有优秀的扩展性、灵活性高
- 访问者模式可以对功能进行统一,可以用来做报表、UI、拦截器和过滤器,适用于数据结构相对稳定的系统
缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特原则所不建议的,这样造成了具体元素变更比较困难
- 违背依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
- 如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的
十六、迭代器模式
16.1 - 基本介绍
- 是常用的设计模式,属于行为模式
- 如果我们的集合元素是用不同方式实现的,有数组,还有
Java
的集合类,或者还有其他方式,当客户端要遍历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决- 迭代器模式,提供一种遍历集合的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层标识
16.2 - 注意事项及细节
优点
- 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
- 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的内部具体组成
- 提供了一种设计思想,就是一个类应该只有一个引起变化的原因,在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。而如果遍历方式改变的话,只影响了迭代器
- 当要展示一组相似对象,或者遍历一组相同对象时使用,适合使用迭代器模式
缺点
- 每个聚合对象都要一个迭代器,会生成多个迭代器不好管理类
十七、观察者模式
17.1 - 原理
- 注册 –
register/add
- 移除 –
remove/delete
- 通知 –
notify
十八、中介者模式
18.1 - 基本介绍
- 用一个中介对象来封装一系列对象交互,中介者使各个对象不需要显式的互相引用,从而使其耦合松散,而且可以独立的改变他们之间的交互
- 中介者模式术语行为型模式,使代码易于维护
18.2 - 注意事项及细节
- 多个类相互耦合,会形成网状结构,使用中介者模式将网状结构分离为星型结构,进行解耦
- 减少类间依赖,降低了耦合,符合迪米特法则
- 中介者承担了较多的责任,一旦中介者出现问题,整个系统就会受到影响
- 如果设计不当,中介者对象本身变得过于复杂
十九、备忘录模式
19.1 - 基本介绍
- 备忘录模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态
- 属于行为型模式
19.2 - 注意事项及细节
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便的回到某个历史的状态
- 实现了信息的封装,使得用户不需要关心状态的保存细节
- 如果类的成员变量过多,势必会占用较大的资源,而且每一次保存都会消耗一定的内存
- 为了节约内存,备忘录模式可以和原型模式配合使用
二十、解释器模式
20.1 - 基本介绍
- 在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看作是解析器
- 是给定一个语言,定义它的语法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子
20.2 - 注意事项及细节
- 当有一个语言需要解释执行,可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程序具有良好的扩展性
- 使用解释器可能带来的问题:解释器模式会引起类膨胀、解释器模式采用递归调用方法,将会导致调试非常复杂、效率可能减低
二十一、状态模式
21.1 - 基本介绍
- 主要是用来解决对象在多种状态转换时,需要对外输出不同的行为的问题,状态和行为是一一对应的,状态之间可以互相转换
- 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
21.2 - 注意事项及细节
- 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
- 方便维护。将容易产生问题的
if-else
语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多if-else
语句,而且容易出错- 符合
OCP
,容易增删状态- 会产生很多类,每个状体都要一个对应的类,当状态过多时会产生很多类,加大维护难度
- 当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求不同的行为的时候,可以考虑使用状态模式
二十二、策略模式
22.1 - 基本介绍
- 定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的用户
- 这算法体现了几个设计原则
- 把变化的代码从不变的代码中分离出来
- 针对接口编程而不是具体类
- 多用组合/聚合,少用继承
22.2 - 注意事项及细节
- 关键是分析项目中变化部分与不变部分
- 策略模式的核心思想是多用组合/聚合,少用继承,用行为类组合,而不是行为的继承,更有弹性
- 体现了
OCP
原则,客户端增加行为不用修改原有代码,只要添加一种策略即可,避免了使用多重转移语句- 提供了可以替换继承关系的办法,策略模式将算法封装在独立的类,使得可以独立于另一个类改变他,使他易于切换、理解、扩展
- 需要注意的是,每添加一个策略就会增加一个类,当策略过多会导致类数目庞大
二十三、职责链模式
23.1 - 基本介绍
- 为请求创建了一个接收者对象的链。这种模式对请求的发送者和接收者进行解耦
- 职责链模式通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么他会把相同的请求传给下一个接收者,依此类推
- 属于行为型模式
23.2 - 注意事项及细节
- 将请求和处理分开,实现解耦,提高系统的灵活性
- 简化了对象,使对象不需要知道链的结构
- 性能会受到影响,特别是链比较长的时候,因此需控制链中最大节点数量
- 调试不方便,采用了类似递归的方式,调试时逻辑可能比较复杂
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Cin's Home!